/*
 * Copyright (c) 2009 John Pritchard, WTKX Project Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wtkx.io;

import wtkx.ui.Threads;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * Implementation of the well known XHR, documented in <a
 * href="http://www.w3.org/TR/XMLHttpRequest/">W3C XMLHttpRequest</a>.
 * 
 * <pre>
 *  client = new XMLHttpRequest();
 *  client.onreadystatechange = new Runnable() {
 * 
 *      if (client.readyState == 4){
 *          //(done)
 *      }
 *  };
 *  client.open("GET", url);
 *  client.send();
 * </pre>
 * 
 * @author jdp
 */
public class XMLHttpRequest 
    extends Object
    implements Runnable
{
    public final static Charset UTF8 = Charset.forName("UTF-8");


    public final static int UNSENT = 0;
    public final static int OPENED = 1;
    public final static int HEADERS_RECEIVED = 2;
    public final static int LOADING = 3;
    public final static int DONE = 4;


    public String responseText;
    public Document responseXML;
    public int status;
    public String statusText;

    public int readyState;

    public Runnable onreadystatechange;

    private String method;
    private URL target;
    private boolean async = true;
    private String authUser, authPass;
    private URLConnection targetConnection;
    private int tid;


    public XMLHttpRequest(){
        super();
    }


    public String getClassName(){
        return "XMLHttpRequest";
    }
    public String getResponseText(){
        return this.responseText;
    }
    public Object getResponseXML(){
        return this.responseXML;
    }
    public Integer getStatus(){
        return this.status;
    }
    public String getStatusText(){
        return this.statusText;
    }
    public Integer getReadyState(){
        return this.readyState;
    }
    public Runnable getOnreadystatechange(){
        return this.onreadystatechange;
    }
    public void setOnreadystatechange(Runnable onreadystatechange){

        this.onreadystatechange = onreadystatechange;
    }
    public void open(String method, String url){
        this.open(method,url,true,null,null);
    }
    public void open(String method, String url, boolean async){
        this.open(method,url,async,null,null);
    }
    public void open(String method, String url, boolean async, String user){
        this.open(method,url,async,user,null);
    }
    public void open(String method, String url, boolean async, String user, String pass){
        try {
            this.method = method;
            URL target = new URL(url);
            this.target = target;
            this.async = async;
            this.authUser = user;
            this.authPass = pass;
            this.targetConnection = target.openConnection();
            this.readyState = OPENED;
        }
        catch (IOException exc){
            this.readyState = UNSENT;
            throw new IllegalArgumentException(url,exc);
        }
    }
    public void setRequestHeader(String name, String value){
        if (null != name && null != value){
            if (OPENED == this.readyState){
                URLConnection connection = this.targetConnection;
                if (null != connection)
                    connection.setRequestProperty(name,value);
            }
            else
                throw new IllegalStateException();
        }
        else
            throw new IllegalArgumentException();
    }
    public void setRequestMethod(String method){
        if (null != method){
            this.method = method;
            URLConnection connection = this.targetConnection;
            if (null != connection){
                HttpURLConnection http;
                if (connection instanceof HttpURLConnection){
                    http = (HttpURLConnection)connection;
                    try {
                        http.setRequestMethod(method);
                    }
                    catch (java.net.ProtocolException exc){
                        throw new IllegalStateException(method,exc);
                    }
                }
            }
        }
        else
            throw new IllegalArgumentException();
    }
    public void maySetRequestProperty(String name, String value){

        URLConnection connection = this.targetConnection;
        if (null != connection){
            if (null == connection.getRequestProperty(name))
                connection.setRequestProperty(name,value);
        }
    }
    public void send(){
        if (OPENED == this.readyState){
            if (null == this.method)
                this.setRequestMethod("GET");
            else
                this.setRequestMethod(this.method);

            try {
                this.read();
            }
            catch (IOException exc){
                exc.printStackTrace();
                this.readyState = DONE;
            }
            this.onready();
        }
        else
            throw new IllegalStateException("readyState: "+this.readyState);
    }
    public void send(String string){
        if (null == string)
            this.send();
        else {
            byte[] buf = string.getBytes(UTF8);

            this.maySetRequestProperty("Content-Type","text/plain;charset=UTF-8");

            this.send(buf);
        }
    }
    public void send(Document doc){
        if (null == doc)
            this.send();
        else {
            DOMOutputBuffer buffer = new DOMOutputBuffer(doc,this.target,UTF8);

            this.maySetRequestProperty("Content-Type","text/xml");

            this.send(buffer.toByteArray());
        }
    }
    public void send(byte[] content){
        if (OPENED == this.readyState){
            if (null == this.method)
                this.setRequestMethod("POST");
            else
                this.setRequestMethod(this.method);

            try {
                this.write(content);
                this.read();
                this.onready();
            }
            catch (IOException exc){
                throw new IllegalStateException(this.target.toExternalForm(),exc);
            }
            finally {
                this.readyState = DONE;
            }
        }
        else
            throw new IllegalStateException("readyState: "+this.readyState);
    }
    public void abort(){

        Threads.ClearTimeout(this.tid);
    }
    public String getAllResponseHeaders(){
        switch (this.readyState){
        case UNSENT:
        case OPENED:
            throw new IllegalStateException();
        default:
            URLConnection connection = this.targetConnection;
            if (null != connection){
                StringWriter buf = new StringWriter();
                PrintWriter out = new PrintWriter(buf);
                Map<String,List<String>> pheaders = connection.getHeaderFields();
                if (null != pheaders){
                    Iterator<String> keys = pheaders.keySet().iterator();
                    while (keys.hasNext()){
                        String key = keys.next();
                        List<String> values = pheaders.get(key);
                        if (null != key && null != values){
                            for (int cc = 0, count = values.size(); cc < count; cc++){
                                String value = values.get(cc);
                                out.print(key);
                                out.print(": ");
                                out.print(value);
                                out.print("\r\n");//(spec)
                            }
                        }
                    }
                }
                return buf.toString();
            }
            else
                return null;
        }
    }
    public String getResponseHeader(String name){
        switch (this.readyState){
        case UNSENT:
        case OPENED:
            throw new IllegalStateException();
        default:
            URLConnection connection = this.targetConnection;
            if (null != connection){
                Map<String,List<String>> pheaders = connection.getHeaderFields();
                if (null != pheaders){
                    List<String> values = pheaders.get(name);
                    if (null != values){
                        return values.get(0);
                    }
                }
            }
            return null;
        }
    }

    protected void read()
        throws IOException
    {

        this.readyState = LOADING;
        URLConnection connection = this.targetConnection;
        if (null != connection){
            HttpURLConnection http;
            if (connection instanceof HttpURLConnection)
                http = (HttpURLConnection)connection;
            else
                http = null;

            InputStream in = null;
            try {
                in = connection.getInputStream();
                if (null != http){
                    this.status = http.getResponseCode();
                    this.statusText = http.getResponseMessage();
                }
                else if (null != in){
                    this.status = 200;
                    this.statusText = "OK";
                }
                else {
                    this.status = 404;
                    this.statusText = "Not Found";
                    this.responseText = null;
                    this.responseXML = null;
                    this.readyState = DONE;
                    return;
                }
                this.readyState = HEADERS_RECEIVED;
                String type = connection.getContentType();
                if (null != type && type.startsWith("text/xml")){
                    this.responseText = null;
                    this.responseXML = ReadXML(in,this.target.toExternalForm());
                }
                else {
                    this.responseText = ReadText(connection,in);
                    this.responseXML = null;
                }
            }
            finally {
                this.readyState = DONE;
                if (null != in){
                    try {
                        in.close();
                    }
                    catch (IOException ignore){
                    }
                }
                if (null != http)
                    http.disconnect();
            }
        }
        this.readyState = DONE;
    }
    protected void write(byte[] content)
        throws IOException
    {
        if (null != content){
            URLConnection connection = this.targetConnection;
            if (null != connection){
                connection.setDoOutput(true);
                connection.setRequestProperty("Content-Length",String.valueOf(content.length));

                OutputStream out = null;
                try {
                    out = connection.getOutputStream();
                    out.write(content,0,content.length);
                    out.flush();
                }
                finally {
                    if (null != out){
                        try {
                            out.close();
                        }
                        catch (IOException ignore){
                        }
                    }
                }
            }
        }
    }
    protected void onready(){
        Runnable user = this.onreadystatechange;
        if (null != user){
            if (this.async)
                this.tid = Threads.SetTimeout(user,0);
            else
                ((Runnable)user).run();
        }
    }
    protected final static String ContentEncoding(URLConnection connection){
        if (null != connection){
            String encoding = connection.getContentEncoding();
            if (null == encoding){
                String type = connection.getContentType();
                if (null != type){
                    int idx = type.indexOf(';');
                    if (0 < idx){
                        idx += 1;
                        if (idx < type.length()){
                            String test = type.substring(idx).trim();
                            if (test.startsWith("charset=")){
                                test = test.substring("charset=".length()).trim();
                                idx = test.indexOf(';');
                                if (-1 < idx){
                                    test = test.substring(0,idx).trim();
                                    if (1 > test.length())
                                        test = null;
                                    else
                                        test = test.toUpperCase();
                                }
                                encoding = test;
                            }
                        }
                    }
                }
            }
            else
                encoding = encoding.toUpperCase();

            return encoding;
        }
        else
            return null;
    }
    protected final static String ReadText(URLConnection connection, InputStream in)
        throws IOException
    {
        int len = connection.getContentLength();
        if (0 < len){
            byte[] buf = new byte[len];
            {
                int bofs = 0, blen = len, read;
                while (bofs < blen && 0 < (read = in.read(buf,bofs,blen))){
                    bofs += read;
                    blen -= read;
                }
            }
            String encoding = ContentEncoding(connection);
            if (null != encoding){
                Charset cs = Charset.forName(encoding);
                return new String(buf,cs);
            }
            else 
                return new String(buf,0,0,len);
        }
        else
            return null;
    }

    private static DocumentBuilderFactory DocBFactory;

    protected final static Document ReadXML(InputStream in, String sid)
        throws IOException
    {
        DocumentBuilder docBuilder;
        DocumentBuilderFactory docBFactory = DocBFactory;
        if (null == docBFactory){
            docBFactory = DocumentBuilderFactory.newInstance();
            DocBFactory = docBFactory;
            docBFactory.setNamespaceAware(true);
            docBFactory.setValidating(false);
        }
        try {
            docBuilder = docBFactory.newDocumentBuilder();
            InputSource src = new InputSource(in);
            src.setSystemId(sid);
            return docBuilder.parse(src);
        }
        catch (javax.xml.parsers.ParserConfigurationException exc){
            throw new IOException("Error configuring XML Document reader.",exc);
        }
        catch (org.xml.sax.SAXException exc){
            throw new IOException("Error reading XML from input.",exc);
        }
    }
    public void run(){
    }
}
